<!DOCTYPE html>
<html lang="en-US">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>多人协作如何进行冲突处理 | 被删的前端游乐场</title>
    <meta name="generator" content="VuePress 1.8.2">
    
    <meta name="description" content="Just playing around">
    
    <link rel="preload" href="/front-end-playground/assets/css/0.styles.6ad2a9ca.css" as="style"><link rel="preload" href="/front-end-playground/assets/js/app.1e2670bf.js" as="script"><link rel="preload" href="/front-end-playground/assets/js/2.38d016d1.js" as="script"><link rel="preload" href="/front-end-playground/assets/js/3.e3f029cb.js" as="script"><link rel="preload" href="/front-end-playground/assets/js/57.090be1c7.js" as="script">
    <link rel="stylesheet" href="/front-end-playground/assets/css/0.styles.6ad2a9ca.css">
  </head>
  <body>
    <div id="app" data-server-rendered="true"><div class="theme-container"><header class="navbar"><div class="sidebar-button"><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img" viewBox="0 0 448 512" class="icon"><path fill="currentColor" d="M436 124H12c-6.627 0-12-5.373-12-12V80c0-6.627 5.373-12 12-12h424c6.627 0 12 5.373 12 12v32c0 6.627-5.373 12-12 12zm0 160H12c-6.627 0-12-5.373-12-12v-32c0-6.627 5.373-12 12-12h424c6.627 0 12 5.373 12 12v32c0 6.627-5.373 12-12 12zm0 160H12c-6.627 0-12-5.373-12-12v-32c0-6.627 5.373-12 12-12h424c6.627 0 12 5.373 12 12v32c0 6.627-5.373 12-12 12z"></path></svg></div> <a href="/front-end-playground/" class="home-link router-link-active"><!----> <span class="site-name">被删的前端游乐场</span></a> <div class="links"><div class="search-box"><input aria-label="Search" autocomplete="off" spellcheck="false" value=""> <!----></div> <nav class="nav-links can-hide"><div class="nav-item"><a href="/front-end-playground/" class="nav-link">概述</a></div><div class="nav-item"><a href="/front-end-playground/front-end-basic/" class="nav-link router-link-active">前端领域</a></div><div class="nav-item"><a href="/front-end-playground/vue/" class="nav-link">Vue学习</a></div><div class="nav-item"><a href="/front-end-playground/wxapp/" class="nav-link">小程序学习</a></div><div class="nav-item"><a href="/front-end-playground/front-end-others/" class="nav-link">百家齐放</a></div><div class="nav-item"><a href="/front-end-playground/front-end-addon/" class="nav-link">前端的进击</a></div><div class="nav-item"><a href="/front-end-playground/front-end-work/" class="nav-link">前端与工作</a></div><div class="nav-item"><a href="/front-end-playground/faq.html" class="nav-link">FAQ</a></div> <a href="https://github.com/godbasin/front-end-playground" target="_blank" rel="noopener noreferrer" class="repo-link">
    Github
    <span><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" class="icon outbound"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg> <span class="sr-only">(opens new window)</span></span></a></nav></div></header> <div class="sidebar-mask"></div> <aside class="sidebar"><nav class="nav-links"><div class="nav-item"><a href="/front-end-playground/" class="nav-link">概述</a></div><div class="nav-item"><a href="/front-end-playground/front-end-basic/" class="nav-link router-link-active">前端领域</a></div><div class="nav-item"><a href="/front-end-playground/vue/" class="nav-link">Vue学习</a></div><div class="nav-item"><a href="/front-end-playground/wxapp/" class="nav-link">小程序学习</a></div><div class="nav-item"><a href="/front-end-playground/front-end-others/" class="nav-link">百家齐放</a></div><div class="nav-item"><a href="/front-end-playground/front-end-addon/" class="nav-link">前端的进击</a></div><div class="nav-item"><a href="/front-end-playground/front-end-work/" class="nav-link">前端与工作</a></div><div class="nav-item"><a href="/front-end-playground/faq.html" class="nav-link">FAQ</a></div> <a href="https://github.com/godbasin/front-end-playground" target="_blank" rel="noopener noreferrer" class="repo-link">
    Github
    <span><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" class="icon outbound"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg> <span class="sr-only">(opens new window)</span></span></a></nav>  <ul class="sidebar-links"><li><section class="sidebar-group collapsable depth-0" style="padding-top:;"><!----> <p class="sidebar-heading"><span>前端架构</span> <span class="arrow right"></span></p> <!----></section></li><li><section class="sidebar-group collapsable depth-0" style="padding-top:10px;"><div class="kitty-main" data-v-2b653b36><span class="stand" data-v-2b653b36></span> <div class="cat" data-v-2b653b36><div class="body" data-v-2b653b36></div> <div class="head" data-v-2b653b36><div class="ear" data-v-2b653b36></div> <div class="ear" data-v-2b653b36></div></div> <div class="face" data-v-2b653b36><div class="nose" data-v-2b653b36></div> <div class="whisker-container" data-v-2b653b36><div class="whisker" data-v-2b653b36></div> <div class="whisker" data-v-2b653b36></div></div> <div class="whisker-container" data-v-2b653b36><div class="whisker" data-v-2b653b36></div> <div class="whisker" data-v-2b653b36></div></div></div> <div class="tail-container" data-v-2b653b36><div class="tail" data-v-2b653b36><div class="tail" data-v-2b653b36><div class="tail" data-v-2b653b36><div class="tail" data-v-2b653b36><div class="tail" data-v-2b653b36><div class="tail" data-v-2b653b36><div class="tail" data-v-2b653b36></div></div></div></div></div></div></div></div></div></div> <p class="sidebar-heading open"><span>前端深入理解</span> <span class="arrow down"></span></p> <ul class="sidebar-links sidebar-group-items"><li><a href="/front-end-playground/front-end-basic/understanding/operation-transform.html" aria-current="page" class="active sidebar-link">多人协作如何进行冲突处理</a><ul class="sidebar-sub-headers"><li class="sidebar-sub-header"><a href="/front-end-playground/front-end-basic/understanding/operation-transform.html#operational-transformation-ot" class="sidebar-link">Operational transformation(OT)</a></li></ul></li><li><a href="/front-end-playground/front-end-basic/understanding/component-communication-with-data-model.html" class="sidebar-link">数据抽离与数据管理</a></li><li><a href="/front-end-playground/front-end-basic/understanding/component-with-configuration.html" class="sidebar-link">组件配置化</a></li><li><a href="/front-end-playground/front-end-basic/understanding/component-with-itself.html" class="sidebar-link">一个组件的自我修养</a></li><li><a href="/front-end-playground/front-end-basic/understanding/app-component-isolation.html" class="sidebar-link">页面区块化与应用组件化</a></li><li><a href="/front-end-playground/front-end-basic/understanding/template-engine.html" class="sidebar-link">前端模板引擎</a></li><li><a href="/front-end-playground/front-end-basic/understanding/dialogue-abstraction.html" class="sidebar-link">对话抽象</a></li><li><a href="/front-end-playground/front-end-basic/understanding/data-driven-or-event-driven.html" class="sidebar-link">前端思维转变--从事件驱动到数据驱动</a></li><li><a href="/front-end-playground/front-end-basic/understanding/web-browser-render.html" class="sidebar-link">浏览器是如何进行页面渲染的</a></li></ul></section></li><li><section class="sidebar-group collapsable depth-0" style="padding-top:;"><!----> <p class="sidebar-heading"><span>前端入门</span> <span class="arrow right"></span></p> <!----></section></li></ul> </aside> <main class="page"> <div class="theme-default-content content__default"><p>最近接触到一些针对多人同时操作进行冲突处理的场景，简单介绍下相关的实现方式。
</p> <h2 id="operational-transformation-ot"><a href="#operational-transformation-ot" class="header-anchor">#</a> Operational transformation(OT)</h2> <p>OT 算法最初是为在纯文本文档的协作编辑中的一致性维护和并发控制而发明的，在本文中我们也主要掌握一致性维护相关的一些方法。</p> <h3 id="协同软件的冲突"><a href="#协同软件的冲突" class="header-anchor">#</a> 协同软件的冲突</h3> <p>想必大家都知道，在多人协同场景下，必然会出现各种各样的冲突场景。</p> <p>举个例子，团队接到一个超大型的项目需要开发，老板说10分钟给出排期和分工。然后瑟瑟发抖的大家二话不说打开腾讯文档，创建了一个表格，让每个模块负责人先针对自己模块来进行工作量拆分和预估。</p> <p>PM 创建了表格之后，将表格丢到群里，说前端后台各自创建一个子表来写相应的工作量情况。</p> <p>前端张三马上点开了表格，点击添加子表，系统自动生成“工作表2”这样一个子表。与此同时，后台李四也进行了同样的操作。</p> <p>那么问题来了，一个表格中原则上并不允许两个同样名字的子表，这个时候冲突就出现了，我们要怎么处理呢？</p> <p>虽然是两个同样名字的子表，但我们并不能将它们进行合并，因为对于张三和李四来说，他们就是在自己创建的子表里写工作拆分和排期情况。所以，我们需要使用对用户影响最小的方式，来解决掉这个冲突。</p> <h3 id="操作的拆分"><a href="#操作的拆分" class="header-anchor">#</a> 操作的拆分</h3> <p>为了处理冲突，我们需要将一些操作进行拆分。例如，我们插入一个子表这样一个操作，除了插入自身的操作，可能需要对其他子表进行移动操作。那么，对于一个子表来说，我们的操作可能会包括：</p> <ul><li>插入</li> <li>重命名</li> <li>移动</li> <li>删除</li> <li>更新内容</li> <li>...</li></ul> <p>只要拆分得足够仔细，对于子表的所有用户行为，都可以由这些操作来组合成最终的效果。例如，复制粘贴一张子表，可以拆分为<code>插入-重命名-更新内容</code>；剪切一张子表，可以拆分为<code>插入-更新内容-删除-移动其他子表</code>。通过分析用户行为，我们可以提取出这些基本操作。</p> <h3 id="操作间的冲突处理"><a href="#操作间的冲突处理" class="header-anchor">#</a> 操作间的冲突处理</h3> <p>基本操作提取出来之后，我们就可以很仔细地梳理和分析操作和操作之间是否会产生冲突，以及要怎么处理了。</p> <p>例如，我们上面提取出来的关于子表的操作中，包括<code>插入</code>、<code>重命名</code>、<code>移动</code>、<code>删除</code>、<code>更新内容</code>五种操作，实际上，每种操作都可能和自身、以及其他四种操作都发生冲突，于是我们可能有<code>5*5=25</code>种需要考虑的冲突情况。</p> <p>我们先来大致看看这 25 组冲突中，是不是全都需要进行冲突处理的。例如，<code>更新内容</code>一般来说跟其他几个操作都不会发生什么冲突，因为更新内容改变的是表格的内容，而不是位置、名字这些，一个表格内部和另一个表格内部基本上不会发生冲突。但<code>重命名</code>和<code>插入</code>之间，满足一定条件的时候（插入的子表名字和重命名的名字相同）可能就会产生冲突。</p> <p>你可能会觉得疑惑，<code>插入-重命名</code>和<code>重命名-插入</code>不是一样的吗？我们先带着这个疑问继续往后看。</p> <h3 id="最终一致性的实现"><a href="#最终一致性的实现" class="header-anchor">#</a> 最终一致性的实现</h3> <p>说了那么多，看起来跟 OT 算法完全没有关系呀？？</p> <p>OT 算法的一个核心目标，是实现最终一致性。为什么会有最终一致性的需求呢？</p> <p>我们再看回张三和李四的例子，由于系统不允许存在有两个同样名称的子表，因此服务器会根据收到请求的顺序，将第二个子表进行重命名。假设张三的请求先到达服务端，那么李四创建的表格则需要被自动重命名为“工作表2（自动重命名）”。</p> <p>为了让用户体验更流畅，我们在用户侧使用无锁、非阻塞的方式来进行协同。也就是对于李四来说，他已经创建了这样一个叫“工作表2”的子表了，由于网络延迟等原因可能还已经编辑上了。这时候服务端告诉李四，张三已经创建了一个“工作表2”的子表了，你自己看着办吧。</p> <p>李四说，我已经编辑了这么多，你总不能让我全删掉重来吧。所以李四想了个办法，先将自己本地的表格<code>重命名</code>为&quot;工作表2（自动重命名）&quot;，然后将张三的子表<code>插入</code>。除此之外，由于自己的插入顺序在后面，还需要将自己的子表<code>移动</code>到后面一个位置。做完这些操作之后，李四告诉服务器，自己也<code>插入</code>了一个叫“工作表2（自动重命名）”的子表。</p> <p>我们梳理下逻辑，可以得到：</p> <ul><li>对于李四本地，需要进行的操作是：<code>重命名 + 插入 + 移动</code></li> <li>对于服务器，需要进行的操作是：<code>插入</code>更新后的子表</li></ul> <p>我们来看看这个 OT 算法的简略说明图：
<img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/google_ot.jpg" alt=""></p> <p>我们代入到张三李四这个场景下：
<img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/sheet_ot.png" alt=""></p> <p>可以看到，对于服务端来说，最终就是新增了两个子表，一个是张三的“工作表2”，另一个是李四的“工作表2（自动重命名）”。</p> <p>除此之外，这个场景中还存在比较细致的时间问题。上面我们说李四收到服务器发来的张三的操作之后，在本地进行<code>重命名 + 插入 + 移动</code>，然后告诉服务器的操作是<code>插入</code>更新后的子表。但是还有个可能性，就是李四收到服务器的消息之前，就已经把自己<code>插入</code>“工作表2”的操作发出去给服务器了。这种情况下，服务器也需要具备处理冲突的能力，来维持最终一致性。</p> <p>也就是说，我们在本地和服务器都有一套一致的冲突处理逻辑，才能保证算法的最终一致性。</p> <p>但除了最终一致性，冲突处理还有其他很多需要考虑的场景，例如版本管理、性能问题等，后面有机会再慢慢介绍吧。</p></div> <!----> <footer class="page-edit"><div class="edit-link"><a href="https://github.com/godbasin/front-end-playground/edit/sourcecode/docs/front-end-basic/understanding/operation-transform.md" target="_blank" rel="noopener noreferrer">帮阿猪改善此页面！</a> <span><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" class="icon outbound"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg> <span class="sr-only">(opens new window)</span></span></div> <!----> <blockquote>部分文章中使用了一些网站的截图，如果涉及侵权，请告诉我删一下谢谢~</blockquote> <div style="margin-top:30px;"><div class="el-row" style="margin-left:-10px;margin-right:-10px;"><div class="el-col el-col-24 el-col-sm-0 el-col-md-2 el-col-lg-4" style="padding-left:10px;padding-right:10px;display:block;"><div style="width:1px;height:1px;"></div></div> <div class="el-col el-col-24 el-col-sm-24 el-col-md-18 el-col-lg-16" style="padding-left:10px;padding-right:10px;"><div class="el-card box-card is-always-shadow"><div class="el-card__header"><div class="clearfix"><span>温馨提示喵</span></div></div><div class="el-card__body"> <div class="el-row" style="margin-left:-10px;margin-right:-10px;"><div class="el-col el-col-24 el-col-xs-24 el-col-sm-12" style="padding-left:10px;padding-right:10px;"><div class="el-image"><div class="image-slot"><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/assets/img/loading.gif" style="width:100%;"></div><!----></div></div> <div class="el-col el-col-24 el-col-xs-24 el-col-sm-12" style="padding-left:10px;padding-right:10px;"><div class="copyright-text"><div>本文版权归作者所有，欢迎转载，但未经作者同意必须保留此段声明，且在文章页面明显位置给出原文连接，否则保留追究法律责任的权利。</div> <div>出处：被删的前端游乐场</div> <div>作者：<a href="https://github.com/godbasin" target="_blank">被删</a></div></div></div></div></div></div></div></div></div></footer> <div class="page-nav"><p class="inner"><span class="prev">
        ←
        <a href="/front-end-playground/front-end-basic/deep-learning/network-design-dependency-decoupling.html" class="prev">
          在线文档的网络层开发思考--依赖关系梳理
        </a></span> <span class="next"><a href="/front-end-playground/front-end-basic/understanding/component-communication-with-data-model.html">
          数据抽离与数据管理
        </a>
        →
      </span></p></div>  <div class="gitalk-container theme-default-content"><div id="gitalk-container" class="content"></div></div></main> <div id="kitty-container"><span><div role="tooltip" id="el-popover-9824" aria-hidden="true" class="el-popover el-popper" style="width:undefinedpx;display:none;"><!----><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/2code2.jpg" class="image"> <div class="text">牡羊猪的猫粮罐</div> </div><span class="el-popover__reference-wrapper"><div id="kitty" style="background:url(https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/assets/img/kitty1.svg);"></div></span></span> <div class="el-dialog__wrapper" style="display:none;"><div role="dialog" aria-modal="true" aria-label="牡羊猪是这样渐渐胖成猪的喵（点击图片可以切换噢）" class="el-dialog" style="margin-top:15vh;"><div class="el-dialog__header"><span class="el-dialog__title">牡羊猪是这样渐渐胖成猪的喵（点击图片可以切换噢）</span><button type="button" aria-label="Close" class="el-dialog__headerbtn"><i class="el-dialog__close el-icon el-icon-close"></i></button></div><!----><!----></div></div></div></div><div class="global-ui"></div></div>
    <script src="/front-end-playground/assets/js/app.1e2670bf.js" defer></script><script src="/front-end-playground/assets/js/2.38d016d1.js" defer></script><script src="/front-end-playground/assets/js/3.e3f029cb.js" defer></script><script src="/front-end-playground/assets/js/57.090be1c7.js" defer></script>
  </body>
</html>
